home *** CD-ROM | disk | FTP | other *** search
/ MacAddict 108 / MacAddict108.iso / Software / Internet & Communication / JunkMatcher 1.5.5.dmg / JunkMatcher.app / Contents / Resources / Engine / Matcher.py < prev    next >
Encoding:
Python Source  |  2005-06-01  |  22.0 KB  |  512 lines

  1. #
  2. #  Matcher.py
  3. #  JunkMatcher
  4. #
  5. #  Created by Benjamin Han on 2/1/05.
  6. #  Copyright (c) 2005 Benjamin Han. All rights reserved.
  7. #
  8.  
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU General Public License
  11. # as published by the Free Software Foundation; either version 2
  12. # of the License, or (at your option) any later version.
  13.  
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17. # GNU General Public License for more details.
  18.  
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  22.  
  23. #!/usr/bin/env python
  24.  
  25. from consts import *
  26. from Tests import *
  27. from MatchResult import *
  28.  
  29. MATCHER_MODE_ALL = 0
  30. MATCHER_MODE_PROPERTIES = 1
  31. MATCHER_MODE_PATTERNS = 2
  32. MATCHER_MODE_LINEAR = 3
  33.  
  34.  
  35. class Matcher:
  36.     def __init__ (self, updateStats = True):
  37.         # sets up self.tests
  38.         # all mentioned files are REQUIRED, EXCEPT for recipientPatterns and safeIPs
  39.         self.tests = Tests(Properties('%sproperties' % CONF_PATH),
  40.                            Patterns('%spatterns' % CONF_PATH),
  41.                            '%stests' % CONF_PATH)
  42.  
  43.         self.mode = globalObjects.prefs.mode
  44.         if self.mode == MATCHER_MODE_LINEAR:
  45.             self.numTests = int(globalObjects.prefs.modeArgs[0])
  46.  
  47.         self.updateStats = updateStats
  48.  
  49.     def setMode (self, mode, *args):
  50.         """mode must be one of the MATCHER_MODE* defined above; each element in
  51.         args must be a string."""
  52.         self.mode = mode
  53.  
  54.         # TO-DO: for now args is needed only for MATCHER_MODE_LINEAR.
  55.         if mode == MATCHER_MODE_LINEAR:
  56.             if len(args): self.numTests = int(args[0])
  57.             else: self.numTests = 1
  58.  
  59.     def run (self, msg):
  60.         matchResult = MatchResult()
  61.         verdict = None    # meaning we haven't reached a verdict
  62.  
  63.         # whitelisting is on for ALL modes
  64.         if msg.m:
  65.             verdict = globalObjects.whitelist.search(msg.sender)
  66.  
  67.             if verdict is not None:
  68.                 # message whitelisted, but we may want to do some testing
  69.                 # for now it's only for PropertyPhishingURL
  70.  
  71.                 # do testing if the message is in HTML and users specified that they want to check
  72.                 # whitelisted emails against PropertyPhishingURL
  73.                 if msg.isHTML and self.tests.properties[u'PropertyPhishingURL'].checkWhitelistedEmail:
  74.                     
  75.                     # find the test for PropertyPhishingURL
  76.                     idx, t = filter(lambda i: not i[1].isPattern and i[1].propertyOrPattern.__class__.__name__ == u'PropertyPhishingURL',
  77.                                     enumerate(self.tests))[0]
  78.  
  79.                     if t.isOn:
  80.                         # only run the properties with matching recipientPattern
  81.                         r = t.propertyOrPattern
  82.                         if not r.recipientPattern or \
  83.                                r.recipientPattern.search('\n'.join(msg.decodedRecipients)):
  84.  
  85.                             result, cpuTime = r.run(msg)
  86.                         
  87.                             if self.mode == MATCHER_MODE_LINEAR:
  88.                                 # this is the only mode where we care hard tests and update statistics
  89.                                 if result is not False:
  90.                                     matchResult.addProperty(r.__class__.__name__, True, result, idx)
  91.                                     if t.isHard or counter == 1:
  92.                                         verdict = True
  93.                                 else:
  94.                                     matchResult.addProperty(r.__class__.__name__, False, testIdx = idx)
  95.                                 
  96.                                 if self.updateStats:
  97.                                     m = matchResult[0]
  98.                                     r.testRecord.addOne(verdict == m.isPositive, cpuTime, m.isPositive)
  99.  
  100.                             elif self.mode != MATCHER_MODE_PATTERNS:
  101.                                 if result is not False:
  102.                                     matchResult.addProperty(r.__class__.__name__, True, result, idx)
  103.                                     verdict = True
  104.                                 else:
  105.                                     matchResult.addProperty(r.__class__.__name__, False, testIdx = idx)
  106.  
  107.         if verdict is None:
  108.  
  109.             if msg.m:
  110.                 decodedRecipients = '\n'.join(msg.decodedRecipients)
  111.  
  112.             # NOTE we allow malformed messages to proceed, til they hit the very first property test
  113.  
  114.             # for ALL modes:
  115.             #   1. apply tests only when they match the specified recipientPattern;
  116.             #   2. apply body/rendering HTML patterns only when the message is in HTML.
  117.             
  118.             if self.mode == MATCHER_MODE_LINEAR:
  119.                 
  120.                 # MATCHER_MODE_LINEAR:
  121.                 #   1. Apply tests only when they match the specified encodingPattern;
  122.                 #   2. Stop running the tests immediately when a hard test gives a positive result.
  123.                 #   3. Actually update the statistics of each test
  124.  
  125.                 knownPositivePatterns = sets.Set()
  126.                 patternConditions = {}    # to cache the results of testing pattern conditions
  127.             
  128.                 counter = self.numTests
  129.                 cpuTimes = []             # for recording the CPU time spent on each test
  130.                 testList = []             # remember what tests we actually performed
  131.                 for idx, test in enumerate(self.tests):
  132.                     if not test.isOn: continue
  133.                 
  134.                     i = test.propertyOrPattern
  135.                     if test.isPattern:
  136.                         # only run the patterns if recipientPattern AND encodingPattern both matches
  137.                         previousResult = patternConditions.get(i.origPattern)
  138.                         if previousResult is None:                        
  139.                             if (i.recipientPattern and i.recipientPattern.search(decodedRecipients) is None)\
  140.                                    or (i.encodingPattern and i.encodingPattern.search(msg.charsets) is None):
  141.                                 patternConditions[i.origPattern] = False
  142.                                 continue
  143.                             patternConditions[i.origPattern] = True
  144.                         elif previousResult is False:
  145.                             continue
  146.                     
  147.                         view = test.view
  148.                         bodyOrRendering = (view == VIEW_BODY or view == VIEW_RENDERING)
  149.  
  150.                         # don't run the body/rendering pattern if the pattern is HTML pattern AND this is NOT an HTML message
  151.                         if bodyOrRendering and test.isHTML and not msg.isHTML: continue
  152.  
  153.                         # we don't want to run same patterns in both body and rendering
  154.                         # because we don't want to doubly penalize a message (body and rendering are similar in nature)
  155.                         if not bodyOrRendering or not i.origPattern in knownPositivePatterns:
  156.                             mo, cpuTime = i.run(msg, view)
  157.                             cpuTimes.append(cpuTime)
  158.                             testList.append(i)
  159.                             
  160.                             if mo:
  161.                                 matchResult.addPattern(i.origPattern, view, True, mo.span(0), idx)
  162.                                 if bodyOrRendering:
  163.                                     knownPositivePatterns.add(i.origPattern)
  164.                                 if test.isHard:
  165.                                     verdict = True
  166.                                     break
  167.                                 else:
  168.                                     counter -= 1
  169.                             else:
  170.                                 matchResult.addPattern(i.origPattern, view, False, testIdx = idx)
  171.                     else:
  172.                         # only run the properties with matching recipientPattern
  173.                         if i.recipientPattern and i.recipientPattern.search(decodedRecipients) is None:
  174.                             continue
  175.                     
  176.                         result, cpuTime = i.run(msg)
  177.                         cpuTimes.append(cpuTime)
  178.                         testList.append(i)
  179.                         
  180.                         if result is not False:                        
  181.                             if result is True: result = None
  182.                             matchResult.addProperty(i.__class__.__name__, True, result, idx)
  183.                             if test.isHard:
  184.                                 verdict = True
  185.                                 break
  186.                             else:
  187.                                 counter -= 1
  188.                         else:
  189.                             matchResult.addProperty(i.__class__.__name__, False, testIdx = idx)
  190.                     
  191.                     if counter == 0:
  192.                         verdict = True
  193.                         break
  194.  
  195.                 if verdict is None: verdict = False
  196.                 
  197.                 # update the statistics of each test
  198.                 for m, cpuTime, test in zip(matchResult, cpuTimes, testList):
  199.                     if m.isProperty:
  200.                         test.testRecord.addOne(verdict == m.isPositive, cpuTime, m.isPositive)
  201.                     else:
  202.                         test.testRecords[m.view].addOne(verdict == m.isPositive,
  203.                                                         cpuTime, m.isPositive)
  204.  
  205.             elif self.mode == MATCHER_MODE_ALL:
  206.                 
  207.                 # MATCHER_MODE_ALL:
  208.                 #   1. We don't distinguis hard/soft tests in this mode.
  209.                 #   2. We don't update the statistics of each test.
  210.  
  211.                 patternConditions = {}    # to cache the results of testing pattern conditions
  212.             
  213.                 for idx, test in enumerate(self.tests):
  214.                     if not test.isOn: continue
  215.                 
  216.                     i = test.propertyOrPattern
  217.                     if test.isPattern:
  218.                         # only run the patterns if recipientPattern AND encodingPattern both matches
  219.                         previousResult = patternConditions.get(i.origPattern)
  220.                         if previousResult is None:
  221.                             if (i.recipientPattern and i.recipientPattern.search(decodedRecipients) is None)\
  222.                                    or (i.encodingPattern and i.encodingPattern.search(msg.charsets) is None):
  223.                                 patternConditions[i.origPattern] = False
  224.                                 continue
  225.                             patternConditions[i.origPattern] = True
  226.                         elif previousResult is False:
  227.                             continue
  228.                         
  229.                         # don't run the body/rendering pattern if the pattern is HTML pattern AND this is NOT an HTML message
  230.                         if (test.view == VIEW_BODY or test.view == VIEW_RENDERING) and test.isHTML and not msg.isHTML:
  231.                             continue
  232.                     
  233.                         view = test.view
  234.                         mo, cpuTime = i.run(msg, view)
  235.                         if mo:
  236.                             verdict = True
  237.                             matchResult.addPattern(i.origPattern, view, True, mo.span(0), idx)
  238.                         else:
  239.                             matchResult.addPattern(i.origPattern, view, False, testIdx = idx)
  240.                     else:
  241.  
  242.                         # only run the properties with matching recipientPattern
  243.                         if i.recipientPattern and i.recipientPattern.search(decodedRecipients) is None:
  244.                             continue
  245.                     
  246.                         result, cpuTime = i.run(msg)
  247.                         if result is not False:
  248.                             verdict = True
  249.                             if result is True: result = None
  250.                             matchResult.addProperty(i.__class__.__name__, True, result, idx)
  251.                         else:
  252.                             matchResult.addProperty(i.__class__.__name__, False, testIdx = idx)
  253.  
  254.                 if verdict is None: verdict = False
  255.  
  256.             elif self.mode == MATCHER_MODE_PROPERTIES:
  257.                 
  258.                 # MATCHER_MODE_PROPERTIES:
  259.                 #   1. We don't distinguis hard/soft tests in this mode.
  260.                 #   2. We don't update the statistics of each test.
  261.                 
  262.                 for idx, test in enumerate(self.tests):
  263.                     if not test.isOn or test.isPattern: continue
  264.  
  265.                     i = test.propertyOrPattern
  266.                     
  267.                     # only run the properties with matching recipientPattern
  268.                     if i.recipientPattern and i.recipientPattern.search(decodedRecipients) is None:
  269.                         continue
  270.                 
  271.                     result, cpuTime = i.run(msg)
  272.                     if result is not False:
  273.                         verdict = True
  274.                         if result is True: result = None
  275.                         matchResult.addProperty(i.__class__.__name__, True, result, idx)
  276.                     else:
  277.                         matchResult.addProperty(i.__class__.__name__, False, testIdx = idx)
  278.  
  279.                 if verdict is None: verdict = False
  280.                         
  281.             elif self.mode == MATCHER_MODE_PATTERNS:
  282.                 
  283.                 # MATCHER_MODE_PATTERNS:
  284.                 #   1. We don't distinguis hard/soft tests in this mode.
  285.                 #   2. We don't update the statistics of each test.
  286.                 
  287.                 if msg.m is None:
  288.                     verdict = None
  289.  
  290.                 else:
  291.                     patternConditions = {}    # to cache the results of testing pattern conditions
  292.                     
  293.                     # we don't distinguis hard/soft tests in this mode
  294.                     for idx, test in enumerate(self.tests):                        
  295.                         if not test.isOn or not test.isPattern: continue
  296.                     
  297.                         i = test.propertyOrPattern
  298.  
  299.                         # only run the patterns if recipientPattern AND encodingPattern both matches
  300.                         previousResult = patternConditions.get(i.origPattern)
  301.                         if previousResult is None:                        
  302.                             if (i.recipientPattern and i.recipientPattern.search(decodedRecipients) is None)\
  303.                                    or (i.encodingPattern and i.encodingPattern.search(msg.charsets) is None):
  304.                                 patternConditions[i.origPattern] = False
  305.                                 continue
  306.                             patternConditions[i.origPattern] = True
  307.                         elif previousResult is False:
  308.                             continue
  309.                         
  310.                         # don't run the body/rendering pattern if the pattern is HTML pattern AND this is NOT an HTML message
  311.                         if (test.view == VIEW_BODY or test.view == VIEW_RENDERING) and test.isHTML and not msg.isHTML:
  312.                             continue
  313.  
  314.                         view = test.view
  315.                         mo, cpuTime = i.run(msg, view)
  316.                         if mo:
  317.                             verdict = True
  318.                             matchResult.addPattern(i.origPattern, view, True, mo.span(0), idx)
  319.                         else:
  320.                             matchResult.addPattern(i.origPattern, view, False, testIdx = idx)
  321.  
  322.                 if verdict is None: verdict = False
  323.  
  324.         matchResult.setVerdict(verdict)
  325.  
  326.         if self.updateStats:
  327.             # update log and emailDB
  328.             k = globalObjects.emailDB.addEntry(msg.msgSrc)
  329.             globalObjects.logger.info(k, msg, matchResult)
  330.  
  331.             if verdict is True and msg.m:
  332.                 msg.addSites()
  333.  
  334.         return matchResult
  335.  
  336.     def getMatchResultStrings (self, msg, matchResult):
  337.         sList = []
  338.         properties = self.tests.properties
  339.         for m in filter(lambda m: m.isPositive, matchResult):
  340.             if hasattr(m, 'info'):
  341.                 if m.isProperty:
  342.                     try:
  343.                         sList.append('- %s: %s' % (properties[m.idStr].name, m.info))
  344.                     except:
  345.                         sList.append('- %s' % properties[m.idStr].name)
  346.                 else:
  347.                     sList.append('- Pattern "%s" matched in view "%s": "%s"' %
  348.                                  (m.idStr, m.view, getattr(msg, m.view)[m.info[0]:m.info[1]]))
  349.             else:
  350.                 sList.append('- %s' % properties[m.idStr].name)
  351.  
  352.         return sList
  353.  
  354.     def recycleLog (self):
  355.         globalObjects.emailDB.recycle()
  356.         globalObjects.logger.recycle()
  357.  
  358.         try:
  359.             os.remove('%scorrections' % CONF_PATH)
  360.         except:
  361.             pass
  362.  
  363.     def recycleLogWhenItsDue (self):
  364.         """Recycles log/emailDB if it's due; returns True iff the recycling did happen."""
  365.         try:
  366.             f = open('%sjm.log' % CONF_PATH)
  367.         except:
  368.             # file might not have been created
  369.             return False
  370.  
  371.         l = f.readline().rstrip()
  372.         if len(l) == 0: return False
  373.         
  374.         try:
  375.             firstDate = __import__('cPickle').loads(f.read(int(l)))[0]
  376.         except Exception, e:
  377.             printException('Exception when trying to load the 1st entry of jm.log', e)
  378.             return False
  379.  
  380.         try:
  381.             delta = datetime.datetime.utcnow() - firstDate
  382.         except Exception, e:
  383.             printException('Exception when determining the date of the 1st log entry', e)
  384.             return False            
  385.  
  386.         if delta.days > globalObjects.prefs.recycleDays:            
  387.             self.recycleLog()
  388.             NSLog(u'JunkMatcher Log is %d day(s) old; recycled.' % delta.days)
  389.             return True
  390.  
  391.         return False
  392.  
  393.     def finalize (self):
  394.         """Call this before the Matcher object is out of commission."""
  395.         self.tests.properties.writeToFile()
  396.         self.tests.patterns.writeToFile()
  397.         globalObjects.siteDB.writeToFile()
  398.  
  399.  
  400. if __name__ == '__main__':
  401.     import sys
  402.  
  403.     def showHelp ():
  404.         print '* Matcher Shell commands:'
  405.         print '  - "?": to show this list again.'
  406.         print '  - "d": to display Matcher settings.'
  407.         print '  - "m <mode #> [arg]": to change mode; arg is optional.'
  408.         print '  - "f <msgFN>": to match a message in file msgFN.'
  409.         print '  - "s <msgFN>": to show the relevant content of an email.'
  410.         print '  - "q": to quit.'
  411.         print '  (<msgFN> can be optionally surrounded by double quotes)'
  412.         print
  413.  
  414.     if len(sys.argv) == 1:
  415.         print '* Logging is off - add a second argument True to turn logging on.'
  416.         matcher = Matcher(False)
  417.     elif len(sys.argv) == 2:
  418.         if sys.argv[1] == 'True':
  419.             print '* Logging is on.'
  420.             matcher = Matcher(True)
  421.         else:
  422.             print '* Logging is off - add a second argument True to turn logging on.'
  423.             matcher = Matcher(False)
  424.     else:
  425.         print '* Usage: ./Matcher.py <logFlag>'
  426.         print '  Set logFlag to True to enable loggin; omitting it will turn logging off.'
  427.         sys.exit(1)
  428.  
  429.     showHelp()
  430.  
  431.     while True:
  432.         try:
  433.             cmd = raw_input('> ').strip()
  434.         except EOFError:
  435.             print
  436.             break
  437.         except Exception, e:
  438.             print e
  439.             break
  440.  
  441.         if cmd == '':
  442.             continue
  443.         
  444.         elif cmd == 'exit' or cmd == 'q' or cmd == 'quit':
  445.             break
  446.  
  447.         elif cmd[0] == '?':
  448.             showHelp()
  449.  
  450.         elif cmd[0] == 'd':            
  451.             if matcher.mode == MATCHER_MODE_LINEAR:
  452.                 print '* Matcher mode: %d (numTests = %d)' % (matcher.mode, matcher.numTests)
  453.             else:
  454.                 print '* Matcher mode: %d' % matcher.mode
  455.             print '* Update stats:', matcher.updateStats
  456.  
  457.         elif cmd[0] == 'm':
  458.             if len(cmd) > 1:
  459.                 cmd = cmd[2:].strip().split(' ')
  460.                 try:
  461.                     mode = int(cmd[0])
  462.                     if mode < 0 or mode > 3: raise Exception()
  463.  
  464.                     if len(cmd) > 1:
  465.                         try:
  466.                             matcher.setMode(mode, cmd[-1])
  467.                         except:
  468.                             print '* [arg] is an integer...'
  469.                     else:
  470.                         matcher.setMode(mode)
  471.                 except:
  472.                     print '* <mode #> is an integer 0 - 3...'
  473.             else:
  474.                 print '* Missing <mode #> [arg] ...'
  475.  
  476.         elif cmd[0] == 'f':
  477.             # match a file
  478.             if len(cmd) > 1:
  479.                 msgFN = cmd[2:].strip()
  480.                 if msgFN[0] == '"': msgFN = msgFN[1:-1]
  481.                 try:
  482.                     msgSrc = open(os.path.expanduser(msgFN)).read()
  483.                 except:
  484.                     print '* Cannot find file "%s"...' % msgFN
  485.                     continue
  486.  
  487.                 print '* old SiteDB size:', globalObjects.siteDB.size()
  488.                 msg = Message(msgSrc)
  489.                 matchResult = matcher.run(msg)
  490.                 print '* Verdict on %s: %s' % (msgFN, matchResult.verdict)
  491.                 print '* new SiteDB size:', globalObjects.siteDB.size()
  492.                     
  493.                 # print all positive tests
  494.                 print encodeText('\n'.join(matcher.getMatchResultStrings(msg, matchResult)))
  495.                                         
  496.             else:
  497.                 print '* Missing <msgFN>...'
  498.  
  499.         elif cmd[0] == 's':
  500.             if len(cmd) > 1:
  501.                 msgFN = cmd[2:].strip()
  502.                 if msgFN[0] == '"': msgFN = msgFN[1:-1]
  503.                 try:
  504.                     Message(open(os.path.expanduser(msgFN)).read()).show()
  505.                 except:
  506.                     print '* Cannot find file "%s"...' % msgFN
  507.             else:
  508.                 print '* Missing <msgFN>...'
  509.             
  510.         else:
  511.             print '* Unknown command "%s"' % cmd
  512.